Crate iroh

Source
Expand description

Peer-to-peer QUIC connections.

iroh is a library to establish direct connectivity between peers. It exposes an interface to QUIC connections and streams to the user, while implementing direct connectivity using hole punching complemented by relay servers under the hood.

An iroh node is created and controlled by the Endpoint, e.g. connecting to another node:

let addr: NodeAddr = todo!();
let ep = Endpoint::builder().bind().await?;
let conn = ep.connect(addr, b"my-alpn").await?;
let mut send_stream = conn.open_uni().await?;
send_stream.write_all(b"msg").await?;

The other node can accept incoming connections using the Endpoint as well:

let ep = Endpoint::builder()
    .alpns(vec![b"my-alpn".to_vec()])
    .bind()
    .await?;
let conn = ep.accept().await.ok_or("err")?.await?;
let mut recv_stream = conn.accept_uni().await?;
let mut buf = [0u8; 3];
recv_stream.read_exact(&mut buf).await?;

Of course you can also use bi-directional streams or any other features from QUIC.

For more elaborate examples, see below or the examples directory in the source repository.

§Connection Establishment

An iroh connection between two iroh nodes is usually established with the help of a Relay server. When creating the Endpoint it connects to the closest Relay server and designates this as the home relay. When other nodes want to connect they first establish connection via this home relay. As soon as connection between the two nodes is established they will attempt to create a direct connection, using hole punching if needed. Once the direct connection is established the relay server is no longer involved in the connection.

If one of the iroh nodes can be reached directly, connectivity can also be established without involving a Relay server. This is done by using the node’s listening addresses in the connection establishement instead of the RelayUrl which is used to identify a Relay server. Of course it is also possible to use both a RelayUrl and direct addresses at the same time to connect.

§Encryption

The connection is encrypted using TLS, like standard QUIC connections. Unlike standard QUIC there is no client, server or server TLS key and certificate chain. Instead each iroh node has a unique SecretKey used to authenticate and encrypt the connection. When an iroh node connects, it uses the corresponding PublicKey to ensure the connection is only established with the intended peer.

Since the PublicKey is also used to identify the iroh node it is also known as the NodeId. As encryption is an integral part of TLS as used in QUIC this NodeId is always a required parameter to establish a connection.

When accepting connections the peer’s NodeId is authenticated. However it is up to the application to decide if a particular peer is allowed to connect or not.

§Relay Servers

Relay servers exist to ensure all iroh nodes are always reachable. They accept encrypted traffic for iroh nodes which are connected to them, forwarding it to the correct destination based on the NodeId only. Since nodes only send encrypted traffic, the Relay servers can not decode any traffic for other iroh nodes and only forward it.

The connections to the Relay server are initiated as normal HTTP 1.1 connections using TLS. Once connected the transport is upgraded to a plain TCP connection using a custom protocol. All further data is then sent using this custom relaying protocol. Usually soon after the connection is established via the Relay it will migrate to a direct connection. However if this is not possible the connection will keep flowing over the relay server as a fallback.

Additionally to providing reliable connectivity between iroh nodes, Relay servers provide some functions to assist in hole punching. They have various services to help nodes understand their own network situation. This includes offering a STUN server, but also a few HTTP extra endpoints as well as responding to ICMP echo requests.

By default the number 0 relay servers are used, see RelayMode::Default.

§Connections and Streams

An iroh node is managed using the Endpoint and this is used to create or accept connections to other nodes. To establish a connection to an iroh node you need to know three pieces of information:

  • The NodeId of the peer to connect to.
  • Some addressing information:
    • Usually the RelayUrl identifying the Relay server.
    • Sometimes, or usually additionally, any direct addresses which might be known.
  • The QUIC/TLS Application-Layer Protocol Negotiation, or ALPN, name to use.

The ALPN is used by both sides to agree on which application-specific protocol will be used over the resulting QUIC connection. These can be protocols like h3 used for HTTP/3, but more commonly will be a custom identifier for the application.

Once connected the API exposes QUIC streams. These are very cheap to create so can be created at any time and can be used to create very many short-lived stream as well as long-lived streams. There are two stream types to choose from:

  • Uni-directional which only allows the peer which initiated the stream to send data.

  • Bi-directional which allows both peers to send and receive data. However, the initiator of this stream has to send data before the peer will be aware of this stream.

Additionally to being extremely light-weight, streams can be interleaved and will not block each other. Allowing many streams to co-exist, regardless of how long they last.

To keep streams cheap, they are lazily created on the network: only once a sender starts sending data on the stream will the receiver become aware of a stream. This means only calling Connection::open_bi is not sufficient for the corresponding call to Connection::accept_bi to return. The sender must send data on the stream before the receiver’s Connection::accept_bi call will return.

§Node Discovery

The need to know the RelayUrl or some direct addresses in addition to the NodeId to connect to an iroh node can be an obstacle. To address this the endpoint::Builder allows to configure a discovery service.

The DnsDiscovery service is a discovery service which will publish the RelayUrl and direct addresses to a service publishing those as DNS records. To connect it looks up the NodeId in the DNS system to find the addressing details. This enables connecting using only the NodeId which is often more convenient and resilient.

See the discovery module for more details.

§Examples

The central struct is the Endpoint, which allows you to connect to other nodes:

use anyhow::Result;
use iroh::{Endpoint, NodeAddr};

async fn connect(addr: NodeAddr) -> Result<()> {
    // The Endpoint is the central object that manages an iroh node.
    let ep = Endpoint::builder().bind().await?;

    // Establish a QUIC connection, open a bi-directional stream, exchange messages.
    let conn = ep.connect(addr, b"hello-world").await?;
    let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
    send_stream.write_all(b"hello").await?;
    send_stream.finish()?;
    let _msg = recv_stream.read_to_end(10).await?;

    // Gracefully close the connection and endpoint.
    conn.close(1u8.into(), b"done");
    ep.close().await?;
    println!("Client closed");
    Ok(())
}

Every Endpoint can also accept connections:

use anyhow::{Context, Result};
use futures_lite::StreamExt;
use iroh::{ticket::NodeTicket, Endpoint, NodeAddr};

async fn accept() -> Result<()> {
    // To accept connections at least one ALPN must be configured.
    let ep = Endpoint::builder()
        .alpns(vec![b"hello-world".to_vec()])
        .bind()
        .await?;

    // Accept a QUIC connection, accept a bi-directional stream, exchange messages.
    let conn = ep.accept().await.context("no incoming connection")?.await?;
    let (mut send_stream, mut recv_stream) = conn.accept_bi().await?;
    let _msg = recv_stream.read_to_end(10).await?;
    send_stream.write_all(b"world").await?;
    send_stream.finish()?;

    // Wait for the client to close the connection and gracefully close the endpoint.
    conn.closed().await;
    ep.close().await?;
    Ok(())
}

Please see the examples directory for more nuanced examples.

Re-exports§

Modules§

  • Default values used in iroh
  • A dialer to conveniently dial many nodes.
  • Node address discovery.
  • This module exports a DNS resolver, which is also the default resolver used in the crate::Endpoint if no custom resolver is configured.
  • The Endpoint allows establishing connections to other iroh nodes.
  • The blake3 hash used in Iroh.
  • Cryptographic key handling for iroh.
  • Co-locating all of the iroh metrics structs
  • Tools for spawning an accept loop that routes incoming requests to the right protocol.
  • test_utilstest or test-utils
    Internal utilities to support testing.
  • TLS configuration based on libp2p TLS specs.

Structs§

  • Network paths to contact an iroh node.
  • Network-level addressing information for an iroh node.
  • Configuration of all the relay servers that can be used.
  • Information on a specific relay server.
  • A URL identifying a relay server.

Enums§

Type Aliases§

  • The identifier for a node in the (iroh) network.